home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / fileutils.py < prev    next >
Encoding:
Python Source  |  2009-04-06  |  17.2 KB  |  505 lines

  1. '''Functions to manage apport problem report files.
  2.  
  3. Copyright (C) 2006 Canonical Ltd.
  4. Author: Martin Pitt <martin.pitt@ubuntu.com>
  5.  
  6. This program is free software; you can redistribute it and/or modify it
  7. under the terms of the GNU General Public License as published by the
  8. Free Software Foundation; either version 2 of the License, or (at your
  9. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  10. the full text of the license.
  11. '''
  12.  
  13. import os, glob, subprocess, os.path, ConfigParser
  14. from problem_report import ProblemReport
  15.  
  16. from packaging_impl import impl as packaging
  17.  
  18. report_dir = os.environ.get('APPORT_REPORT_DIR', '/var/crash')
  19.  
  20. _config_file = '~/.config/apport/settings'
  21.  
  22. def find_package_desktopfile(package):
  23.     '''If given package is installed and has a single .desktop file, return the
  24.     path to it, otherwise return None.'''
  25.  
  26.     if package is None:
  27.         return None
  28.  
  29.     desktopfile = None
  30.  
  31.     for line in packaging.get_files(package):
  32.         if line.endswith('.desktop'):
  33.             if desktopfile:
  34.                 return None # more than one
  35.             else:
  36.                 desktopfile = line
  37.  
  38.     return desktopfile
  39.  
  40. def likely_packaged(file):
  41.     '''Check whether the given file is likely to belong to a package.
  42.  
  43.     This is semi-decidable: A return value of False is definitive, a True value
  44.     is only a guess which needs to be checked with find_file_package().
  45.     However, this function is very fast and does not access the package
  46.     database.'''
  47.  
  48.     pkg_whitelist = ['/bin/', '/boot', '/etc/', '/initrd', '/lib', '/sbin/',
  49.     '/usr/', '/var'] # packages only ship files in these directories
  50.  
  51.     whitelist_match = False
  52.     for i in pkg_whitelist:
  53.         if file.startswith(i):
  54.             whitelist_match = True
  55.             break
  56.     return whitelist_match and not file.startswith('/usr/local/') and not \
  57.         file.startswith('/var/lib/schroot')
  58.  
  59. def find_file_package(file):
  60.     '''Return the package that ships the given file (or None if no package
  61.     ships it).'''
  62.  
  63.     # resolve symlinks in directories
  64.     (dir, name) = os.path.split(file)
  65.     resolved_dir = os.path.realpath(dir)
  66.     if os.path.isdir(resolved_dir):
  67.         file = os.path.join(resolved_dir, name)
  68.  
  69.     if not likely_packaged(file):
  70.         return None
  71.  
  72.     return packaging.get_file_package(file)
  73.  
  74. def seen_report(report):
  75.     '''Check whether the given report file has already been processed
  76.     earlier.'''
  77.  
  78.     st = os.stat(report)
  79.     return (st.st_atime > st.st_mtime) or (st.st_size == 0)
  80.  
  81. def mark_report_seen(report):
  82.     '''Mark given report file as seen.'''
  83.  
  84.     st = os.stat(report)
  85.     try:
  86.         os.utime(report, (st.st_mtime, st.st_mtime-1))
  87.     except OSError:
  88.         # file is probably not our's, so do it the slow and boring way
  89.         # change the file's access time until it stat's different than the mtime.
  90.         # This might take a while if we only have 1-second resolution. Time out
  91.         # after 1.2 seconds.
  92.         timeout = 12
  93.         while timeout > 0:
  94.             f = open(report)
  95.             f.read(1)
  96.             f.close()
  97.             try:
  98.                 st = os.stat(report)
  99.             except OSError:
  100.                 return
  101.  
  102.             if st.st_atime > st.st_mtime:
  103.                 break
  104.             time.sleep(0.1)
  105.             timeout -= 1
  106.  
  107.         if timeout == 0:
  108.             # happens on noatime mounted partitions; just give up and delete
  109.             delete_report(report)
  110.  
  111. def get_all_reports():
  112.     '''Return a list with all report files which are accessible to the calling
  113.     user.'''
  114.  
  115.     reports = []
  116.     for r in glob.glob(os.path.join(report_dir, '*.crash')):
  117.         try:
  118.             if os.path.getsize(r) > 0 and os.access(r, os.R_OK):
  119.                 reports.append(r)
  120.         except OSError:
  121.             # race condition, can happen if report disappears between glob and
  122.             # stat
  123.             pass
  124.     return reports
  125.  
  126. def get_new_reports():
  127.     '''Return a list with all report files which have not yet been processed
  128.     and are accessible to the calling user.'''
  129.  
  130.     return [r for r in get_all_reports() if not seen_report(r)]
  131.  
  132. def get_all_system_reports():
  133.     '''Return a list with all report files which belong to a system user (i. e.
  134.     uid < 500 according to LSB).'''
  135.  
  136.     reports = []
  137.     for r in glob.glob(os.path.join(report_dir, '*.crash')):
  138.         try:
  139.             if os.path.getsize(r) > 0 and os.stat(r).st_uid < 500:
  140.                 reports.append(r)
  141.         except OSError:
  142.             # race condition, can happen if report disappears between glob and
  143.             # stat
  144.             pass
  145.     return reports
  146.  
  147. def get_new_system_reports():
  148.     '''Return a list with all report files which have not yet been processed
  149.     and belong to a system user (i. e. uid < 500 according to LSB).'''
  150.  
  151.     return [r for r in get_all_system_reports() if not seen_report(r)]
  152.  
  153. def delete_report(report):
  154.     '''Delete the given report file.
  155.  
  156.     If unlinking the file fails due to a permission error (if report_dir is not
  157.     writable to normal users), the file will be truncated to 0 bytes instead.'''
  158.  
  159.     try:
  160.         os.unlink(report)
  161.     except OSError:
  162.         open(report, 'w').truncate(0)
  163.  
  164. def get_recent_crashes(report):
  165.     '''Return the number of recent crashes for the given report file.
  166.  
  167.     Return the number of recent crashes (currently, crashes which happened more
  168.     than 24 hours ago are discarded).'''
  169.  
  170.     pr = ProblemReport()
  171.     pr.load(report, False)
  172.     try:
  173.         count = int(pr['CrashCounter'])
  174.         report_time = time.mktime(time.strptime(pr['Date']))
  175.         cur_time = time.mktime(time.localtime())
  176.         # discard reports which are older than 24 hours
  177.         if cur_time - report_time > 24*3600:
  178.             return 0
  179.         return count
  180.     except (ValueError, KeyError):
  181.         return 0
  182.  
  183. def make_report_path(report, uid=None):
  184.     '''Construct a canonical pathname for the given report.
  185.  
  186.     If uid is not given, it defaults to the uid of the current process.'''
  187.  
  188.     if report.has_key('ExecutablePath'):
  189.         subject = report['ExecutablePath'].replace('/', '_')
  190.     elif report.has_key('Package'):
  191.         subject = report['Package'].split(None, 1)[0]
  192.     else:
  193.         raise ValueError, 'report has neither ExecutablePath nor Package attribute'
  194.  
  195.     if not uid:
  196.         uid = os.getuid()
  197.  
  198.     return os.path.join(report_dir, '%s.%i.crash' % (subject, uid))
  199.  
  200. def check_files_md5(sumfile):
  201.     '''Given a list of MD5 sums in md5sum(1) format (relative to /), check
  202.     integrity of all files and return a list of files that don't match.'''
  203.  
  204.     assert os.path.exists(sumfile)
  205.     m = subprocess.Popen(['/usr/bin/md5sum', '-c', sumfile],
  206.         stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True,
  207.         cwd='/', env={})
  208.     out = m.communicate()[0]
  209.  
  210.     # if md5sum succeeded, don't bother parsing the output
  211.     if m.returncode == 0:
  212.         return []
  213.  
  214.     mismatches = []
  215.     for l in out.splitlines():
  216.         if l.endswith('FAILED'):
  217.             mismatches.append(l.rsplit(':', 1)[0])
  218.  
  219.     return mismatches
  220.  
  221. def get_config(section, setting, default=None, bool=False):
  222.     '''Return a setting from user configuration.
  223.  
  224.     This is read from ~/.config/apport/settings. If bool is True, the value is
  225.     interpreted as a boolean.
  226.     '''
  227.     if not get_config.config:
  228.         get_config.config = ConfigParser.ConfigParser()
  229.         get_config.config.read(os.path.expanduser(_config_file))
  230.  
  231.     try:
  232.         if bool:
  233.             return get_config.config.getboolean(section, setting)
  234.         else:
  235.             return get_config.config.get(section, setting)
  236.     except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
  237.         return default
  238.  
  239. get_config.config = None
  240.  
  241. #
  242. # Unit test
  243. #
  244.  
  245. import unittest, tempfile, os, shutil, sys, time
  246. from cStringIO import StringIO
  247.  
  248. class _ApportUtilsTest(unittest.TestCase):
  249.     def setUp(self):
  250.         global report_dir
  251.         global _config_file
  252.         self.orig_report_dir = report_dir
  253.         report_dir = tempfile.mkdtemp()
  254.         self.orig_config_file = _config_file
  255.  
  256.     def tearDown(self):
  257.         global report_dir
  258.         global _config_file
  259.         shutil.rmtree(report_dir)
  260.         report_dir = self.orig_report_dir
  261.         self.orig_report_dir = None
  262.         _config_file = self.orig_config_file
  263.  
  264.     def _create_reports(self, create_inaccessible = False):
  265.         '''Create some test reports.'''
  266.  
  267.         r1 = os.path.join(report_dir, 'rep1.crash')
  268.         r2 = os.path.join(report_dir, 'rep2.crash')
  269.  
  270.         open(r1, 'w').write('report 1')
  271.         open(r2, 'w').write('report 2')
  272.         os.chmod(r1, 0600)
  273.         os.chmod(r2, 0600)
  274.         if create_inaccessible:
  275.             ri = os.path.join(report_dir, 'inaccessible.crash')
  276.             open(ri, 'w').write('inaccessible')
  277.             os.chmod(ri, 0)
  278.             return [r1, r2, ri]
  279.         else:
  280.             return [r1, r2]
  281.  
  282.     def test_find_package_desktopfile(self):
  283.         '''find_package_desktopfile().'''
  284.  
  285.         # package without any .desktop file
  286.         nodesktop = 'bash'
  287.         assert len([f for f in packaging.get_files(nodesktop)
  288.             if f.endswith('.desktop')]) == 0
  289.  
  290.         # find a package with one and a package with multiple .desktop files
  291.         onedesktop = None
  292.         multidesktop = None
  293.         for d in os.listdir('/usr/share/applications/'):
  294.             if not d.endswith('.desktop'):
  295.                 continue
  296.             pkg = packaging.get_file_package(
  297.                 os.path.join('/usr/share/applications/', d))
  298.             num = len([f for f in packaging.get_files(pkg)
  299.                 if f.endswith('.desktop')])
  300.             if not onedesktop and num == 1:
  301.                 onedesktop = pkg
  302.             elif not multidesktop and num > 1:
  303.                 multidesktop = pkg
  304.  
  305.             if onedesktop and multidesktop:
  306.                 break
  307.  
  308.         if nodesktop:
  309.             self.assertEqual(find_package_desktopfile(nodesktop), None, 'no-desktop package %s' % nodesktop)
  310.         if multidesktop:
  311.             self.assertEqual(find_package_desktopfile(multidesktop), None, 'multi-desktop package %s' % multidesktop)
  312.         if onedesktop:
  313.             d = find_package_desktopfile(onedesktop)
  314.             self.assertNotEqual(d, None, 'one-desktop package %s' % onedesktop)
  315.             self.assert_(os.path.exists(d))
  316.             self.assert_(d.endswith('.desktop'))
  317.  
  318.     def test_likely_packaged(self):
  319.         '''likely_packaged().'''
  320.  
  321.         self.assertEqual(likely_packaged('/bin/bash'), True)
  322.         self.assertEqual(likely_packaged('/usr/bin/foo'), True)
  323.         self.assertEqual(likely_packaged('/usr/local/bin/foo'), False)
  324.         self.assertEqual(likely_packaged('/home/test/bin/foo'), False)
  325.         self.assertEqual(likely_packaged('/tmp/foo'), False)
  326.         # err on the side of caution for /var
  327.         self.assertEqual(likely_packaged('/var/lib/foo'), True)
  328.         # but ignore temporary schroot session chroots
  329.         # (https://launchpad.net/bugs/122859)
  330.         self.assertEqual(likely_packaged('/var/lib/schroot/bin/bash'), False)
  331.  
  332.     def test_find_file_package(self):
  333.         '''find_file_package().'''
  334.  
  335.         self.assertEqual(find_file_package('/bin/bash'), 'bash')
  336.         self.assertEqual(find_file_package('/bin/cat'), 'coreutils')
  337.         self.assertEqual(find_file_package('/nonexisting'), None)
  338.  
  339.     def test_seen(self):
  340.         '''get_new_reports() and seen_report().'''
  341.  
  342.         self.assertEqual(get_new_reports(), [])
  343.         if os.getuid() == 0:
  344.             tr = self._create_reports(True)
  345.         else:
  346.             tr = [r for r in self._create_reports(True) if not 'inaccessible' in r]
  347.         self.assertEqual(set(get_new_reports()), set(tr))
  348.  
  349.         # now mark them as seen and check again
  350.         nr = set(tr)
  351.         for r in tr:
  352.             self.assertEqual(seen_report(r), False)
  353.             nr.remove(r)
  354.             mark_report_seen(r)
  355.             self.assertEqual(seen_report(r), True)
  356.             self.assertEqual(set(get_new_reports()), nr)
  357.  
  358.     def test_get_all_reports(self):
  359.         '''get_all_reports().'''
  360.  
  361.         self.assertEqual(get_all_reports(), [])
  362.         if os.getuid() == 0:
  363.             tr = self._create_reports(True)
  364.         else:
  365.             tr = [r for r in self._create_reports(True) if not 'inaccessible' in r]
  366.         self.assertEqual(set(get_all_reports()), set(tr))
  367.  
  368.         # now mark them as seen and check again
  369.         for r in tr:
  370.             mark_report_seen(r)
  371.  
  372.         self.assertEqual(set(get_all_reports()), set(tr))
  373.  
  374.     def test_get_system_reports(self):
  375.         '''get_all_system_reports() and get_new_system_reports().'''
  376.  
  377.         self.assertEqual(get_all_reports(), [])
  378.         self.assertEqual(get_all_system_reports(), [])
  379.         if os.getuid() == 0:
  380.             tr = self._create_reports(True)
  381.             self.assertEqual(set(get_all_system_reports()), set(tr))
  382.             self.assertEqual(set(get_new_system_reports()), set(tr))
  383.  
  384.             # now mark them as seen and check again
  385.             for r in tr:
  386.                 mark_report_seen(r)
  387.  
  388.             self.assertEqual(set(get_all_system_reports()), set(tr))
  389.             self.assertEqual(set(get_new_system_reports()), set([]))
  390.         else:
  391.             tr = [r for r in self._create_reports(True) if not 'inaccessible' in r]
  392.             self.assertEqual(set(get_all_system_reports()), set([]))
  393.             self.assertEqual(set(get_new_system_reports()), set([]))
  394.  
  395.     def test_delete_report(self):
  396.         '''delete_report().'''
  397.  
  398.         tr = self._create_reports()
  399.  
  400.         while tr:
  401.             self.assertEqual(set(get_all_reports()), set(tr))
  402.             delete_report(tr.pop())
  403.  
  404.     def test_get_recent_crashes(self):
  405.         '''get_recent_crashes().'''
  406.  
  407.         # incomplete fields
  408.         r = StringIO('''ProblemType: Crash''')
  409.         self.assertEqual(get_recent_crashes(r), 0)
  410.  
  411.         r = StringIO('''ProblemType: Crash
  412. Date: Wed Aug 01 00:00:01 1990''')
  413.         self.assertEqual(get_recent_crashes(r), 0)
  414.  
  415.         # ancient report
  416.         r = StringIO('''ProblemType: Crash
  417. Date: Wed Aug 01 00:00:01 1990
  418. CrashCounter: 3''')
  419.         self.assertEqual(get_recent_crashes(r), 0)
  420.  
  421.         # old report (one day + one hour ago)
  422.         r = StringIO('''ProblemType: Crash
  423. Date: %s
  424. CrashCounter: 3''' % time.ctime(time.mktime(time.localtime())-25*3600))
  425.         self.assertEqual(get_recent_crashes(r), 0)
  426.  
  427.         # current report (one hour ago)
  428.         r = StringIO('''ProblemType: Crash
  429. Date: %s
  430. CrashCounter: 3''' % time.ctime(time.mktime(time.localtime())-3600))
  431.         self.assertEqual(get_recent_crashes(r), 3)
  432.  
  433.     def test_make_report_path(self):
  434.         '''make_report_path().'''
  435.  
  436.         pr = ProblemReport()
  437.         self.assertRaises(ValueError, make_report_path, pr)
  438.  
  439.         pr['Package'] = 'bash 1'
  440.         self.assert_(make_report_path(pr).startswith('%s/bash' % report_dir))
  441.         pr['ExecutablePath'] = '/bin/bash';
  442.         self.assert_(make_report_path(pr).startswith('%s/_bin_bash' % report_dir))
  443.  
  444.     def test_check_files_md5(self):
  445.         '''check_files_md5().'''
  446.  
  447.         f1 = os.path.join(report_dir, 'test 1.txt')
  448.         f2 = os.path.join(report_dir, 'test:2.txt')
  449.         sumfile = os.path.join(report_dir, 'sums.txt')
  450.         open(f1, 'w').write('Some stuff')
  451.         open(f2, 'w').write('More stuff')
  452.         # use one relative and one absolute path in checksums file
  453.         open(sumfile, 'w').write('''2e41290da2fa3f68bd3313174467e3b5  %s
  454. f6423dfbc4faf022e58b4d3f5ff71a70  %s
  455. ''' % (f1[1:], f2))
  456.         self.assertEqual(check_files_md5(sumfile), [], 'correct md5sums')
  457.  
  458.         open(f1, 'w').write('Some stuff!')
  459.         self.assertEqual(check_files_md5(sumfile), [f1[1:]], 'file 1 wrong')
  460.         open(f2, 'w').write('More stuff!')
  461.         self.assertEqual(check_files_md5(sumfile), [f1[1:], f2], 'files 1 and 2 wrong')
  462.         open(f1, 'w').write('Some stuff')
  463.         self.assertEqual(check_files_md5(sumfile), [f2], 'file 2 wrong')
  464.  
  465.     def test_get_config(self):
  466.         '''get_config().'''
  467.  
  468.         global _config_file
  469.  
  470.         # nonexisting
  471.         _config_file = '/nonexisting'
  472.         self.assertEqual(get_config('main', 'foo'), None)
  473.         self.assertEqual(get_config('main', 'foo', 'moo'), 'moo')
  474.         get_config.config = None # trash cache
  475.  
  476.         # empty
  477.         f = tempfile.NamedTemporaryFile()
  478.         _config_file = f.name
  479.         self.assertEqual(get_config('main', 'foo'), None)
  480.         self.assertEqual(get_config('main', 'foo', 'moo'), 'moo')
  481.         get_config.config = None # trash cache
  482.  
  483.         # nonempty
  484.         f.write('[main]\none=1\ntwo = TWO\nb1 = 1\nb2=False\n[spethial]\none= 99\n')
  485.         f.flush()
  486.         self.assertEqual(get_config('main', 'foo'), None)
  487.         self.assertEqual(get_config('main', 'foo', 'moo'), 'moo')
  488.         self.assertEqual(get_config('main', 'one'), '1')
  489.         self.assertEqual(get_config('main', 'one', default='moo'), '1')
  490.         self.assertEqual(get_config('main', 'two'), 'TWO')
  491.         self.assertEqual(get_config('main', 'b1', bool=True), True)
  492.         self.assertEqual(get_config('main', 'b2', bool=True), False)
  493.         self.assertEqual(get_config('main', 'b3', bool=True), None)
  494.         self.assertEqual(get_config('main', 'b3', default=False, bool=True), False)
  495.         self.assertEqual(get_config('spethial', 'one'), '99')
  496.         self.assertEqual(get_config('spethial', 'two'), None)
  497.         self.assertEqual(get_config('spethial', 'one', 'moo'), '99')
  498.         self.assertEqual(get_config('spethial', 'nope', 'moo'), 'moo')
  499.         get_config.config = None # trash cache
  500.  
  501.         f.close()
  502.  
  503. if __name__ == '__main__':
  504.     unittest.main()
  505.